#include "gif.h"

#ifdef __cplusplus
extern "C"{
#endif

uint_fast8_t fileRead(GifFileType *gif, GifByteType *bytes, uint_fast8_t size) {
    FILE *file = (FILE *) gif->UserData;
    return (uint_fast8_t) fread(bytes, 1, size, file);
}

uint_fast8_t directByteBufferRead(GifFileType *gif, GifByteType *bytes, uint_fast8_t size) {
    DirectByteBufferContainer *dbbc = (DirectByteBufferContainer *)gif->UserData;
    if (dbbc->position + size > dbbc->capacity) {
        size -= dbbc->position + size - dbbc->capacity;
    }
    memcpy(bytes, dbbc->bytes + dbbc->position, (size_t) size);
    dbbc->position += size;
    return size;
}

uint_fast8_t byteArrayRead(GifFileType *gif, GifByteType *bytes, uint_fast8_t size) {
    ByteArrayContainer *bac = (ByteArrayContainer *)gif->UserData;
    JNIEnv *env = getEnv();
    if (env == NULL) {
        return 0;
    }
    if (bac->position + size > bac->length) {
        size -= bac->position + size - bac->length;
    }
    env->GetByteArrayRegion(bac->buffer, (jsize) bac->position, size, (jbyte *) bytes);
    bac->position += size;
    return size;
}

static jint bufferUpTo(JNIEnv *env, StreamContainer *sc, size_t size) {
    jint totalLength = 0;
    jint length;
    do {
        length = env->CallIntMethod(sc->stream, sc->readMethodID, sc->buffer, totalLength, size - totalLength);
        if (length > 0) {
            totalLength += length;
        } else {
            if (env->ExceptionCheck() == JNI_TRUE) {
#ifdef DEBUG
                env->ExceptionDescribe();
#endif
                env->ExceptionClear();
            }
            break;
        }
    } while (totalLength < size);
    return totalLength;
}

uint_fast8_t streamRead(GifFileType *gif, GifByteType *bytes, uint_fast8_t size) {
    StreamContainer *sc = (StreamContainer *)gif->UserData;
    JNIEnv *env = getEnv();
    if (env == NULL || env->MonitorEnter(sc->stream) != 0) {
        return 0;
    }

    if (sc->bufferPosition == 0) {
        size_t bufferSize = sc->markCalled ? STREAM_BUFFER_SIZE : size;
        jint readLen = bufferUpTo(env, sc, bufferSize);
        if (readLen < size) {
            size = (uint_fast8_t) readLen;
        }
        env->GetByteArrayRegion( sc->buffer, 0, size, (jbyte *) bytes);
        if (sc->markCalled) {
            sc->bufferPosition += size;
        }
    } else if (sc->bufferPosition + size <= STREAM_BUFFER_SIZE) {
        env->GetByteArrayRegion( sc->buffer, sc->bufferPosition, size, (jbyte *) bytes);
        sc->bufferPosition += size;
    } else {
        jint tailSize = STREAM_BUFFER_SIZE - sc->bufferPosition;
        env->GetByteArrayRegion( sc->buffer, sc->bufferPosition, tailSize, (jbyte *) bytes);
        bytes += tailSize;
        jint readLen = bufferUpTo(env, sc, STREAM_BUFFER_SIZE);
        jint headSize = size - tailSize;
        if (readLen < headSize) {
            size = (uint_fast8_t) readLen;
            headSize = readLen;
        }
        env->GetByteArrayRegion( sc->buffer, 0, headSize, (jbyte *) bytes);
        sc->bufferPosition = headSize;
    }

    if (env->MonitorExit( sc->stream) != 0) {
        return 0;
    }

    return (uint_fast8_t) size;
}

int fileRewind(GifInfo *info) {
    if (fseeko((FILE *)info->gifFilePtr->UserData, info->startPos, SEEK_SET) == 0) {
        return 0;
    }
    info->gifFilePtr->Error = D_GIF_ERR_REWIND_FAILED;
    return -1;
}

int streamRewind(GifInfo *info) {
    GifFileType *gif = info->gifFilePtr;
    StreamContainer *sc = (StreamContainer *)gif->UserData;
    JNIEnv *env = getEnv();
    sc->bufferPosition = 0;
    if (env == NULL) {
        info->gifFilePtr->Error = D_GIF_ERR_REWIND_FAILED;
        return -1;
    }
    env->CallVoidMethod(sc->stream, sc->resetMethodID);
    if (env->ExceptionCheck() == JNI_TRUE) {
#ifdef DEBUG
        env->ExceptionDescribe();
#endif
        env->ExceptionClear();
        info->gifFilePtr->Error = D_GIF_ERR_REWIND_FAILED;
        return -1;
    }
    return 0;
}

int byteArrayRewind(GifInfo *info) {
    ByteArrayContainer *bac = (ByteArrayContainer *)info->gifFilePtr->UserData;
    bac->position = (uint_fast32_t) info->startPos;
    return 0;
}

int directByteBufferRewind(GifInfo *info) {
    DirectByteBufferContainer *dbbc = (DirectByteBufferContainer *)info->gifFilePtr->UserData;
    dbbc->position = info->startPos;
    return 0;
}

JNIEXPORT jlong JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_openFile(JNIEnv *env, jclass fclass, jstring jfname) {
    if (isSourceNull((void *)jfname, env)) {
        return NULL_GIF_INFO;
    }

    const char *const filename = env->GetStringUTFChars(jfname, NULL);
    if (filename == NULL) {
        throwException(env, RUNTIME_EXCEPTION_BARE, "GetStringUTFChars failed");
        return NULL_GIF_INFO;
    }
    FILE *file = fopen(filename, "rbe");
    if (file == NULL) {
        throwGifIOException(D_GIF_ERR_OPEN_FAILED, env, true);
        env->ReleaseStringUTFChars(jfname, filename);
        return NULL_GIF_INFO;
    }
    env->ReleaseStringUTFChars( jfname, filename);

    struct stat64 st;
    const long long sourceLength = stat64(filename, &st) == 0 ? st.st_size : -1;

    GifInfo *const info = createGifInfoFromFile(env, file, sourceLength);
    if (info == NULL) {
        fclose(file);
    }
    return (jlong) (intptr_t) info;
}

JNIEXPORT jlong JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_openByteArray(JNIEnv *env, jclass fclass, jbyteArray bytes) {
    if (isSourceNull(bytes, env)) {
        return NULL_GIF_INFO;
    }
    ByteArrayContainer *container = (ByteArrayContainer *)malloc(sizeof(ByteArrayContainer));
    if (container == NULL) {
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }
    container->buffer = (jbyteArray)env->NewGlobalRef(bytes);
    if (container->buffer == NULL) {
        free(container);
        throwException(env, RUNTIME_EXCEPTION_BARE, "NewGlobalRef failed");
        return NULL_GIF_INFO;
    }
    container->length = (unsigned int) env->GetArrayLength(container->buffer);
    container->position = 0;
    GifSourceDescriptor descriptor;
    descriptor.Error = 0;
    descriptor.rewindFunc = (RewindFunc)byteArrayRewind;
    descriptor.sourceLength = container->length;
    descriptor.GifFileIn = DGifOpen(container, &byteArrayRead, &descriptor.Error);
    descriptor.startPos = container->position;

    GifInfo *info = createGifInfo(&descriptor, env);

    if (info == NULL) {
        env->DeleteGlobalRef(container->buffer);
        free(container);
    }
    return (jlong) (intptr_t) info;
}

 JNIEXPORT jlong JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_openDirectByteBuffer(JNIEnv *env, jclass fclass, jobject buffer) {
    jbyte *bytes = (jbyte *)env->GetDirectBufferAddress( buffer);
    jlong capacity = env->GetDirectBufferCapacity(buffer);
    if (bytes == NULL || capacity <= 0) {
        if (!isSourceNull(buffer, env)) {
            throwGifIOException(D_GIF_ERR_INVALID_BYTE_BUFFER, env, false);
        }
        return NULL_GIF_INFO;
    }
    DirectByteBufferContainer *container = (DirectByteBufferContainer *)malloc(sizeof(DirectByteBufferContainer));
    if (container == NULL) {
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }
    container->bufferRef = env->NewGlobalRef(buffer);
    if (container->bufferRef == NULL) {
        free(container);
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }
    container->bytes = bytes;
    container->capacity = capacity;
    container->position = 0;

    GifSourceDescriptor descriptor;
     descriptor.Error = 0;
     descriptor.rewindFunc = (RewindFunc)directByteBufferRewind;
     descriptor.sourceLength = container->capacity;
    descriptor.GifFileIn = DGifOpen(container, &directByteBufferRead, &descriptor.Error);
    descriptor.startPos = container->position;

    GifInfo *info = createGifInfo(&descriptor, env);
    if (info == NULL) {
        env->DeleteGlobalRef(container->bufferRef);
        free(container);
    }
    return (jlong) (intptr_t) info;
}

JNIEXPORT jlong JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_openStream(JNIEnv *env, jclass fclass, jobject stream) {
    jbyteArray bufferArray = env->NewByteArray( STREAM_BUFFER_SIZE);
    if (bufferArray == NULL) {
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }

    StreamContainer *container = (StreamContainer *)malloc(sizeof(StreamContainer));
    if (container == NULL) {
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }

    container->buffer = (jbyteArray) env->NewGlobalRef(bufferArray);
    if (container->buffer == NULL) {
        free(container);
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }

    jclass streamClass = env->GetObjectClass(stream);
    if (streamClass == NULL) {
        env->DeleteGlobalRef(container->buffer);
        free(container);
        throwException(env, RUNTIME_EXCEPTION_BARE, "NewGlobalRef failed");
        return NULL_GIF_INFO;
    }

    jmethodID markMethodID = env->GetMethodID(streamClass, "mark", "(I)V");
    container->readMethodID = env->GetMethodID( streamClass, "read", "([BII)I");
    container->resetMethodID = env->GetMethodID( streamClass, "reset", "()V");
    container->closeMethodID = env->GetMethodID( streamClass, "close", "()V");

    if (markMethodID == NULL || container->readMethodID == NULL || container->resetMethodID == NULL || container->closeMethodID == NULL) {
        env->DeleteGlobalRef(container->buffer);
        free(container);
        return NULL_GIF_INFO;
    }

    container->stream = env->NewGlobalRef( stream);
    if (container->stream == NULL) {
        env->DeleteGlobalRef( container->buffer);
        free(container);
        throwException(env, RUNTIME_EXCEPTION_BARE, "NewGlobalRef failed");
        return NULL_GIF_INFO;
    }

    GifSourceDescriptor descriptor;
    descriptor.rewindFunc = streamRewind;
    descriptor.startPos = 0;
    descriptor.sourceLength = -1;
    container->bufferPosition = 0;
    container->markCalled = false;
    descriptor.GifFileIn = DGifOpen(container, &streamRead, &descriptor.Error);

    env->CallVoidMethod(stream, markMethodID, INT32_MAX);

    if (env->ExceptionCheck() == JNI_TRUE) {
#ifdef DEBUG
        env->ExceptionDescribe();
#endif
        env->DeleteGlobalRef(container->stream);
        env->DeleteGlobalRef(container->buffer);
        free(container);
        return NULL_GIF_INFO;
    } else {
        GifInfo *info = createGifInfo(&descriptor, env);
        container->markCalled = true;
        container->bufferPosition = 0;
        return (jlong) (intptr_t) info;
    }
}

JNIEXPORT jlong JNICALL
Java_com_example_myapplication_slice_MainAbilitySlice_openStream(JNIEnv *env, jclass fclass, jobject stream) {
    jbyteArray bufferArray = env->NewByteArray(STREAM_BUFFER_SIZE);
    if (bufferArray == NULL) {
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }

    StreamContainer *container = (StreamContainer *)malloc(sizeof(StreamContainer));
    if (container == NULL) {
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }

    container->buffer = (jbyteArray)env->NewGlobalRef(bufferArray);
    if (container->buffer == NULL) {
        free(container);
        throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
        return NULL_GIF_INFO;
    }

    jclass streamClass = env->GetObjectClass(stream);
    if (streamClass == NULL) {
        env->DeleteGlobalRef(container->buffer);
        free(container);
        throwException(env, RUNTIME_EXCEPTION_BARE, "NewGlobalRef failed");
        return NULL_GIF_INFO;
    }

    jmethodID markMethodID = env->GetMethodID(streamClass, "mark", "(I)V");
    container->readMethodID = env->GetMethodID(streamClass, "read", "([BII)I");
    container->resetMethodID = env->GetMethodID( streamClass, "reset", "()V");
    container->closeMethodID = env->GetMethodID( streamClass, "close", "()V");

    if (markMethodID == NULL || container->readMethodID == NULL || container->resetMethodID == NULL || container->closeMethodID == NULL) {
        env->DeleteGlobalRef(container->buffer);
        free(container);
        return NULL_GIF_INFO;
    }

    container->stream = env->NewGlobalRef(stream);
    if (container->stream == NULL) {
        env->DeleteGlobalRef(container->buffer);
        free(container);
        throwException(env, RUNTIME_EXCEPTION_BARE, "NewGlobalRef failed");
        return NULL_GIF_INFO;
    }

    GifSourceDescriptor descriptor;
    descriptor.rewindFunc = streamRewind;
    descriptor.startPos = 0;
    descriptor.sourceLength =-1;

    container->bufferPosition = 0;
    container->markCalled = false;
    descriptor.GifFileIn = DGifOpen(container, &streamRead, &descriptor.Error);

    env->CallVoidMethod(stream, markMethodID, INT32_MAX);

    if (env->ExceptionCheck() == JNI_TRUE) {
#ifdef DEBUG
        env->ExceptionDescribe();
#endif
        env->DeleteGlobalRef(container->stream);
        env->DeleteGlobalRef(container->buffer);
        free(container);
        return NULL_GIF_INFO;
    } else {
        GifInfo *info = createGifInfo(&descriptor, env);
        container->markCalled = true;
        container->bufferPosition = 0;
        return (jlong) (intptr_t) info;
    }
}


JNIEXPORT jint JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_extractNativeFileDescriptor(JNIEnv *env, jclass handleClass, jobject fileDescriptor, jboolean closeOriginalDescriptor) {
    if (isSourceNull(fileDescriptor, env)) {
        return -1;
    }
    jclass fdClass = env->GetObjectClass(fileDescriptor);
    static jfieldID fdClassDescriptorFieldID = NULL;
    if (fdClassDescriptorFieldID == NULL) {
        fdClassDescriptorFieldID = env->GetFieldID(fdClass, "descriptor", "I");
    }
    if (fdClassDescriptorFieldID == NULL) {
        return -1;
    }
    const jint oldFd = env->GetIntField(fileDescriptor, fdClassDescriptorFieldID);
    const int fd = dup(oldFd);
    if (fd == -1) {
        throwGifIOException(D_GIF_ERR_OPEN_FAILED, env, true);
    }
    if (closeOriginalDescriptor == JNI_TRUE) {
        close(oldFd);
    }
    return fd;
}

JNIEXPORT jint JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_createTempNativeFileDescriptor(JNIEnv *env, jclass handleClass) {
    return eventfd(0, 0);
}
JNIEXPORT jint JNICALL
Java_com_example_myapplication_slice_MainAbilitySlice_createTempNativeFileDescriptor(JNIEnv *env, jclass handleClass) {
    return eventfd(0, 0);
}

JNIEXPORT jlong JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_openNativeFileDescriptor(JNIEnv *env, jclass handleClass, jint fd, jlong offset) {
    if (lseek64(fd, offset, SEEK_SET) != -1) {
        FILE *file = fdopen(fd, "rb");
        if (file == NULL) {
            throwGifIOException(D_GIF_ERR_OPEN_FAILED, env, true);
            close(fd);
            return NULL_GIF_INFO;
        }
        struct stat64 st;
        const long long sourceLength = fstat64(fd, &st) == 0 ? st.st_size : -1;

        GifInfo *const info = createGifInfoFromFile(env, file, sourceLength);
        if (info == NULL) {
            close(fd);
        }
        return (jlong) info;
    } else {
        throwGifIOException(D_GIF_ERR_OPEN_FAILED, env, true);
        close(fd);
        return NULL_GIF_INFO;
    }
}

static GifInfo *createGifInfoFromFile(JNIEnv *env, FILE *file, const long long sourceLength) {
    GifSourceDescriptor descriptor;
    descriptor.rewindFunc = fileRewind;
    descriptor.sourceLength = sourceLength;
    descriptor.GifFileIn = DGifOpen(file, &fileRead, &descriptor.Error);
    descriptor.startPos = ftell(file);

    return createGifInfo(&descriptor, env);
}
#ifdef __cplusplus
}
#endif